Add metrics tracking to the device pairing flow - #1170
Conversation
The pairing handshake emitted no product analytics on either side (the joiner sheet wasn't even screen-tracked). Wire the new device_pairing_* CoreActions events (convos-shared#7) into both pairing view models: one started event plus exactly one terminal completed/failed event per attempt, with failure step, reason (error/expired/cancelled) and duration as properties. A shared DevicePairingMetricsTracker drives this from each view model's flowState didSet, so every path into a terminal state (stream errors, coordinator failures, countdown expiry, redelivered identity shares, cancel-after-terminal dismissals) is counted once without per-site tracking calls. coreActions is threaded from AppSettingsView into DevicesViewModel (settings entry) and from ConversationsViewModel into the joiner sheet and the respond-to-join-request initiator sheet. ConvosCore temporarily pins convos-shared to the feature branch; flip back to main once convos-shared#7 merges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed pin changes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @yewreeka's task —— View job Code Review Complete ✅I've reviewed PR #1170 and the implementation looks solid overall. Here's my detailed feedback: Tasks
✅ Strengths1. Excellent State Machine Design The
2. Smart Integration Pattern Driving metrics from
3. Proper DI Threading The dependency injection is clean:
4. Code Quality
🔍 Minor Issues & Suggestions1. Type Annotation for Clarity (DevicePairingMetricsTracker.swift:74-76) The private var durationSecs: Float {
guard let startedAt else { return 0 }
let interval: TimeInterval = Date().timeIntervalSince(startedAt)
return Float(interval)
}This follows CLAUDE.md guidance: "Annotate the type on any non-trivial 2. Missing Comments on State Machine Guarantees While the doc comment on the class is excellent, the critical /// False when the attempt never started or already emitted its
/// terminal event. The dual guard ensures:
/// 1. No double-counting (didFinish)
/// 2. No terminal events before started() was called (startedAt check)
private func finishOnce() -> Bool {3. Consistency: Role Property Access In let role = role
Task { await actions.devicePairingStarted(role: role) }But in private func emitAsync(_ block: @escaping (any CoreActions) -> Task<Void, Never>) {
let actions = coreActions
let role = role
block(actions)
}However, the current pattern is fine - just noting for consistency consideration. 4. Test Coverage Gap No unit tests for
Example test structure: final class DevicePairingMetricsTrackerTests: XCTestCase {
func testCompletedOnlyFiresOnce() async {
let mockActions = MockCoreActions()
let tracker = DevicePairingMetricsTracker(role: .initiator, coreActions: mockActions)
tracker.started()
tracker.completed()
tracker.completed() // Second call
// Assert mockActions.devicePairingCompleted called exactly once
}
}📋 Architecture & Best Practices✅ Follows CLAUDE.md conventions:
✅ Performance:
✅ Security:
🎯 Testing RecommendationsHigh Priority:
Medium Priority: 📦 Merge Order NoteThe PR description correctly notes the temporary
SummaryThis is well-crafted code with a clever design pattern. The state-machine-driven metrics approach is elegant and maintainable. The only gaps are:
The implementation is ready to merge after the Verdict: ✅ Approved with minor suggestions |
There was a problem hiding this comment.
🟡 Medium Devices/PairingSheetViewModel.swift:102
In .respondToJoinRequest mode, if the flow fails before any flowState transition after init (e.g. pairingService.start() throws in startRespondFlow), the devicePairingFailed metric reports step as .qrDisplayed even though this mode never shows a QR. The DevicePairingMetricsTracker defaults lastStep to .qrDisplayed for .initiator, and self.flowState = .syncing in init doesn't fire didSet (Swift skips property observers during initialization), so the tracker never learns the flow started in .syncing. Consider calling metrics.reached(.syncing) in the .respondToJoinRequest branch of init.
if case .respondToJoinRequest = mode {
// Respond mode never shows a QR; start in the spinner state
// so the sheet doesn't flash the empty QR layout while the
// pairing service bootstraps toward `.showingPin`.
self.flowState = .syncing
metrics.reached(.syncing)
}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @Convos/Devices/PairingSheetViewModel.swift around lines 102-107:
In `.respondToJoinRequest` mode, if the flow fails before any `flowState` transition after init (e.g. `pairingService.start()` throws in `startRespondFlow`), the `devicePairingFailed` metric reports `step` as `.qrDisplayed` even though this mode never shows a QR. The `DevicePairingMetricsTracker` defaults `lastStep` to `.qrDisplayed` for `.initiator`, and `self.flowState = .syncing` in `init` doesn't fire `didSet` (Swift skips property observers during initialization), so the tracker never learns the flow started in `.syncing`. Consider calling `metrics.reached(.syncing)` in the `.respondToJoinRequest` branch of `init`.
Summary
The device pairing flow (QR + PIN + emoji handshake) emitted no product analytics on either side — the joiner sheet wasn't even screen-tracked. This wires up the new
device_pairing_*CoreActions events from xmtplabs/convos-shared#7:device_pairing_startedroledevice_pairing_completedrole,duration_secsdevice_pairing_failedrole,reason(error | expired | cancelled),step,duration_secsHow
DevicePairingMetricsTrackerguarantees at most one started + exactly one terminal event per attempt. Both view models drive it fromflowState'sdidSet, so every path into a terminal state (stream errors, coordinator failures, countdown expiry, redelivered identity shares, cancel-after-terminal dismissals) is counted once, without per-site tracking calls. Explicit calls are onlystarted()(flow kickoff) andcancelled()(user dismissal, no-op after a terminal event).coreActionsthreaded via existing DI:AppSettingsView→DevicesViewModel→ initiator sheet (Settings → Devices entry), andConversationsViewModel→ joiner sheet + respond-to-join-request initiator sheet (deep link / iCloud-discovery entries). Defaults toNoOpCoreActions()so previews/tests are unaffected.createInviteandrespondToJoinRequest).Merge order
ConvosCore/Package.swifttemporarily pinsconvos-sharedto the feature branch (based on8b5f741, the revision dev already pins, so this builds today). After xmtplabs/convos-shared#7 merges, flip the pin back tobranch: "main"and re-resolve before landing this.Verification
xcodebuild buildof Convos (Dev) for iOS Simulator (arm64): succeeds, no new warnings, no long-type-check warnings.🤖 Generated with Claude Code
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Add metrics tracking to the device pairing flow for both initiator and joiner roles
DevicePairingMetricsTracker, aMainActorclass that tracks a single pairing attempt and emitsdevicePairingStarted,devicePairingCompleted, ordevicePairingFailedevents viaCoreActions, with role, step, reason, and duration.PairingSheetViewModel(initiator) andJoinerPairingSheetViewModel(joiner), mapping flow state transitions to metric steps and terminal events.coreActionsthroughDevicesViewModel,ConversationsViewModel, andAppSettingsViewso all pairing entry points supply a liveCoreActionsinstance.CoreActionsmethods toNoOpCoreActions, keeping existing callers unaffected.convos-sharedpackage dependency to thejarod/device-pairing-metricsbranch inPackage.swift.📊 Macroscope summarized c2eff02. 2 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.